package arkham.data;

import java.awt.Color;
import java.awt.Image;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.util.*;

import arkham.data.Globals.GateColor;
import arkham.data.Globals.OtherWorldColorSet;
import arkham.jaxb.board.*;
import arkham.util.AHLogger;
import arkham.util.ImageUtils;
import arkham.util.XmlTools;

/**
 * This class represents the game board.
 * @author ctheng
 *
 */
public class Board {

	public final static String SKY_NAME = "Sky";
	public final static String OUTSKIRTS_NAME = "Outskirts";
	public final static String LITAS_NAME = "Lost in Time and Space";
	public final static String DUNWICH_VORTEX_NAME = "Dunwich Vortex";
	public final static String INNSMOUTH_VORTEX_NAME = "Innsmouth Vortex";
	public final static String MAIN_BOARD_NAME = "Arkham Horror Main Board";

	public final static String[] RESTRICTED_LOCATION_NAMES =
	{
		SKY_NAME, OUTSKIRTS_NAME, DUNWICH_VORTEX_NAME, INNSMOUTH_VORTEX_NAME,
		LITAS_NAME
	};
	private HashMap<String, Location> nodes;
	private List<Location> locations;
	private List<OtherWorld> otherWorlds;
	private HashMap<String, LocationContainer> locContainers;
	private String name;

	public Board()
	{
		nodes = new HashMap<String, Location>();
		locations = new ArrayList<Location>();
		otherWorlds = new ArrayList<OtherWorld>();
		locContainers = new HashMap<String, LocationContainer>();
	}

	/**
	 * Initializes the board and populates a list of locations with all
	 * locations on this board, including special locations.<br>
	 * To be called by GameData only.
	 * @param list the list of locations
	 */
	public void init(List<LocationContainer> list)
	{
		for (Location loc : locations)
		{
			locContainers.put(loc.getName(), loc);
		}
		for (OtherWorld ow : otherWorlds)
		{
			locContainers.put(ow.getSegmentName(1), ow.getSegment(1));
			locContainers.put(ow.getSegmentName(2), ow.getSegment(2));
		}
		// create sky and outskirts
		locContainers.put(SKY_NAME, new LocationContainer(SKY_NAME));
		locContainers.put(OUTSKIRTS_NAME, new LocationContainer(OUTSKIRTS_NAME));
		locContainers.put(LITAS_NAME, new LocationContainer(LITAS_NAME));
		if (list != null)
		{
			for (Map.Entry<String, LocationContainer> entry : locContainers.entrySet())
			{
				list.add(entry.getValue());
			}
		}
	}

	/**
	 * Sets up board by placing a clue on each unstable location.
	 */
	public void setupBoard()
	{
		for (Location loc : locations)
		{
			if (!loc.isStable())
			{
				loc.setClues(1);
			}
			else
			{
				loc.setClues(0);
			}
		}
	}

	/**
	 * Gets the name of the board
	 * @return The name of the board
	 */
	public String getName()
	{
		return name;
	}

	/**
	 * Gets the list of locations on the board. Excludes any
	 * special locations
	 * @return the list of locations
	 */
	public List<Location> getLocations()
	{
		return Collections.unmodifiableList(locations);
	}

	/**
	 * Gets the list of other worlds
	 * @return the list of other worlds
	 */
	public List<OtherWorld> getOtherWorlds()
	{
		return Collections.unmodifiableList(otherWorlds);
	}

	/**
	 * Gets the location container containing monsters and investigators
	 * @param name the name of the location container
	 * @return the location container
	 */
	public LocationContainer getLocationContainer(String name)
	{
		return locContainers.get(name);
	}

	/**
	 * Gets an other world by name
	 * @param owname the name of the other world
	 * @return the other world if it exists, or null if it doesn't
	 */
	public OtherWorld getOtherWorld(String owname)
	{
		for (OtherWorld ow : otherWorlds)
		{
			if (ow.getName().equals(owname))
			{
				return ow;
			}
		}
		return null;
	}

	/**
	 * Checks if this board contains the particular location
	 * @param locName the location name
	 * @return true if the board has this location, false otherwise
	 */
	public boolean hasLocation(String locName)
	{
		return nodes.containsKey(locName);
	}

	/**
	 * Gets a particular location
	 * @param locname the location name
	 * @return The location, or null if not found
	 */
	public Location getLocation(String locName)
	{
		return nodes.get(locName);
	}

	/**
	 * Gets the total number of open gates
	 * @return the number of open gates
	 */
	public int getNumOpenGates()
	{
		int count = 0;
		for (Location loc : locations)
		{
			if (loc.hasOpenGate())
			{
				count++;
			}
		}
		return count;
	}

	/**
	 * Gets the total number of sealed gates
	 * @return the number of sealed gates
	 */
	public int getNumSeals()
	{
		int count = 0;
		for (Location loc : locations)
		{
			if (loc.isSealed())
			{
				count++;
			}
		}
		return count;
	}

	/**
	 * Gets the total number of monsters, not including outskirts and sky
	 * @return the number of monsters
	 */
	public int getNumMonsters()
	{
		int count = 0;
		for (Location loc : locations)
		{
			count += loc.getMonsterList().size();
		}
		return count;
	}

	/**
	 * Gets the number of monsters in the sky. This counts towards the
	 * monster limit.
	 * @return the number of monsters in the sky.
	 */
	public int getNumMonstersInSky()
	{
		LocationContainer loc = getLocationContainer(SKY_NAME);
		if (loc != null)
		{
			return loc.getMonsterList().size();
		}
		else return 0;
	}

	/**
	 * Gets all locations with open gates
	 * @return the list of locations with open gates
	 */
	public List<Location> getAllOpenGates()
	{
		ArrayList<Location> list = new ArrayList<Location>();
		for (Location loc : locations)
		{
			GateTile gt = loc.getGate();
			if (gt != null)
			{
				list.add(loc);
			}
		}
		return list;
	}

	/**
	 * Gets all locations with open gates to this other world
	 * @param otherWorld the other world's name
	 * @return the list of locations with open gates
	 */
	public List<Location> getOpenGatesTo(String otherWorld)
	{
		ArrayList<Location> list = new ArrayList<Location>();
		for (Location loc : locations)
		{
			GateTile gt = loc.getGate();
			if (gt != null && gt.getName().equals(otherWorld))
			{
				list.add(loc);
			}
		}
		return list;
	}

	/**
	 * This class represents a location on the board.
	 * It encompasses the following info:
	 * <ul>
	 * <li>Basics: name, neighborhood, stability, whether it is a street
	 * <li>Alternate text and power if any
	 * <li>x and y position on the board and relevant image
	 * <li>Links to adjacent locations and color of monster movement
	 * arrows.
	 * <li>Color of neighborhood</li>
	 * <li>In game data: number of clues and other location effects
	 * </ul>
	 *
	 * @author ctheng
	 *
	 */
	public static class Location extends LocationContainer
	{
		/** Restricted name of the Dunwich vortex. Locations may not use
		 * restricted names.
		 */
		public final static Location DUNWICH_VORTEX =
			new Location(DUNWICH_VORTEX_NAME);

		/** Restricted name of the Innsmouth vortex. Locations may not use
		 * restricted names.
		 */
		public final static Location INNSMOUTH_VORTEX =
			new Location(INNSMOUTH_VORTEX_NAME);

		/* The following fields are fixed info */
		private String boardName;
		private String description;
		private String neighborhood;
		private String altPower;
		private String altText;
		private AltEventType altEvent;
		private boolean stable, street, station, aquatic;
		private ArrayList<Location> edges;
		private ArrayList<Point> edgeConnectors;
		private int x, y;
		private BufferedImage img;
		private Location blackEdge;
		private Location whiteEdge;
		private Color color;
		private List<String> commonFinds, guaranteedFinds;

		/* The following fields are game related info and can change during
		 * the game */
		private int clues;
		private boolean closed, sealed;
		private GateTile openGate;
		private HashSet<String> effects;

		public Location(String name)
		{
			super(name);
			closed = false;
			sealed = false;
			openGate = null;
			clues = 0;
			effects = new HashSet<String>();
			edges = new ArrayList<Location>();
			edgeConnectors = new ArrayList<Point>();
			blackEdge = null;
			whiteEdge = null;
			altPower = "";
			altText = "";
			commonFinds = new ArrayList<String>(2);
			guaranteedFinds = new ArrayList<String>(2);
		}

		/**
		 * Gets the neighborhood of the location.
		 * The name of the neighborhood is always the name of the street
		 * adjacent to the location
		 * @return the neighborhood
		 */
		public String getNeighborhood() {
			return neighborhood;
		}

		/**
		 * Gets the name of the board this location belongs to.
		 * @return The name of the board
		 */
		public String getBoardName()
		{
			return boardName;
		}

		/**
		 * Returns true if this is a stable location or street
		 * @return true if the location is stable or a street
		 */
		public boolean isStable() {
			return stable;
		}
		/**
		 * Returns true if this is a street, false if it is a location
		 * @return true if it is a street, false otherwise
		 */
		public boolean isStreet() {
			return street;
		}

		/**
		 * Returns true if this location is a train/bus station, false
		 * otherwise
		 * @return true if this location is a station
		 */
		public boolean isStation()
		{
			return station;
		}

		/**
		 * Returns true if this location is aquatic, false otherwise
		 * @return true if this location is aquatic
		 */
		public boolean isAquatic()
		{
			return aquatic;
		}

		/**
		 * Gets the alternate encounter text of this location
		 * @return the alternate encounter text of this location
		 */
		public String getAlternateText()
		{
			return altText;
		}

		/**
		 * Gets the name of the alternate encounter's power of this location
		 * @return the name of the alternate encounter of this location
		 */
		public String getAlternatePower()
		{
			return altPower;
		}

		/**
		 * Gets the alternate event of this location, if any
		 * @return the alternate event of this location, or null if there is none
		 */
		public AltEventType getAlternateEvent()
		{
		    return altEvent;
		}

		/**
		 * Gets the shortened form of the name for display purposes
		 * @return the description
		 */
		public String getDescription()
		{
			return description;
		}

		/**
		 * Gets the image of this location. Streets do not have images
		 * @return the image of the location
		 */
		public Image getImage()
		{
			return img;
		}

		/**
		 * Initializes the image for this location from a file
		 * @param filename The image file name
		 */
		public void setImage(String filename)
		{
			if (filename == null) return;
			img = ImageUtils.loadImage(filename);
			if (img != null)
			{
				img = ImageUtils.resizeImage(img, 50, 50);
			}
		}


		/**
		 * The x location in the graphical representation
		 * @return the x coordinate
		 */
		public int getX()
		{
			return x;
		}

		/**
		 * The y location in the graphical representation
		 * @return the y coordinate
		 */
		public int getY()
		{
			return y;
		}

		/**
		 * Gets the list of edges
		 * @return the list of edges
		 */
		public List<Location> getEdges()
		{
			return edges;
		}

		/**
		 * Checks if this location has a path to the other location
		 * @param locName the location name
		 * @return true if this location has a path to the other location,
		 * false otherwise
		 */
		public boolean hasEdgeTo(String locName)
		{
			for (Location loc : edges)
			{
				if (loc.getName().equals(locName))
				{
					return true;
				}
			}
			return false;
		}

		/**
		 * Gets the list of edge connectors of the graphical representation.
		 * @return the list of edge connectors
		 */
		public List<Point> getEdgeConnectors()
		{
			return edgeConnectors;
		}

		/**
		 * Gets the point of the edge connector to that location in the
		 * graphical representation, or null if none exist
		 * @return the point of the edge connector
		 */
		public Point getEdgeConnectorTo(String locName)
		{
			for (int i = 0; i < edges.size(); i++)
			{
				if (edges.get(i).getName().equals(locName))
				{
					return edgeConnectors.get(i);
				}
			}
			return null;
		}

		/**
		 * Gets the location monsters move on black
		 * @return the location pointed to by the black arrow
		 */
		public Location getBlackEdge()
		{
			return blackEdge;
		}

		/**
		 * Gets the location monsters move on white
		 * @return the location pointed to by the white arrow
		 */
		public Location getWhiteEdge()
		{
			return whiteEdge;
		}

		/**
		 * Returns true if this location is closed, false otherwise
		 * @return true if the location is closed.
		 */
		public boolean isClosed()
		{
			return closed;
		}

		/**
		 * Sets if this location is closed
		 * @param isClosed set to true to close this location
		 */
		public void setClosed(boolean isClosed)
		{
			closed = isClosed;
		}

		/**
		 * Returns true if this location is sealed with an elder sign
		 * @return true if the location is sealed, false otherwise
		 */
		public boolean isSealed()
		{
			return sealed;
		}

		/**
		 * Sets if this location is sealed with an elder sign
		 * @param isSealed set to true to seal this location
		 */
		public void setSealed(boolean isSealed)
		{
			sealed = isSealed;
		}

		/**
		 * Checks if there is an open gate on the location
		 * @return true if there is an open gate, false otherwise
		 */
		public boolean hasOpenGate()
		{
			return openGate != null;
		}

		/**
		 * Gets the open gate tile, if any
		 * @return the open gate tile, or null if no gate is open
		 */
		public GateTile getGate()
		{
			return openGate;
		}

		/**
		 * Opens a new gate on this location and removes all clues present.
		 * @param gate the gate to open
		 */
		public void openNewGate(GateTile gate)
		{
			openGate = gate;
			removeClues();
		}

		/**
		 * Close the gate and retrieve the gate trophy
		 * @return the gate tile of the closed gate
		 */
		public GateTile closeGate()
		{
			GateTile temp = openGate;
			openGate = null;
			return temp;
		}

		/**
		 * Gets the number of clues on this location
		 * @return The number of clues on this location
		 */
		public int getNumClues()
		{
			return clues;
		}

		/**
		 * Sets the number of clues on this location. Clues cannot exist
		 * simultaneously with an open gate.
		 * @param numClues the number of clues to set
		 */
		public void setClues(int numClues)
		{
			if (hasOpenGate())
			{
				return;
			}
			if (numClues >= 0)
			{
				clues = numClues;
			}
		}

		/**
		 * Adds a clue to this location
		 */
		public void addClue()
		{
			clues++;
		}

		/**
		 * Remove all clues from this location
		 * @return the number of clues removed
		 */
		public int removeClues()
		{
			int numClues = clues;
			clues = 0;
			return numClues;
		}

		/**
		 * Checks if this location has an effect active
		 * @param effect The effect to check
		 * @return true if the effect is active, false otherwise
		 */
		public boolean checkEffect(String effect)
		{
			return effects.contains(effect);
		}

		/**
		 * Adds an effect to this location
		 * @param effect the effect to add
		 */
		public void addEffect(String effect)
		{
			effects.add(effect);
		}

		/**
		 * Removes an effect from this location
		 * @param effect the effect to remove
		 */
		public void removeEffect(String effect)
		{
			effects.remove(effect);
		}

		/**
		 * Gets a list of stuff commonly found at this encounter
		 * @return the list of common finds
		 */
		public List<String> getCommonFinds()
		{
			return commonFinds;
		}

		/**
		 * Gets a list of stuff guaranteed by the alternate power of this
		 * encounter
		 * @return the list of guaranteed finds
		 */
		public List<String> getGuaranteedFinds()
		{
			return guaranteedFinds;
		}

		/**
		 * Gets the color of the location
		 * @return the color of the location
		 */
		public Color getColor()
		{
			return color;
		}

		@Override
		public boolean isArkhamLocation()
		{
			return true;
		}

		@Override
		public String toString()
		{
			return name + " (" + clues + "C)" + (closed ? "# " : " ") +
				effects.toString();
		}

		@Override
		public boolean equals(Object o)
		{
			if (o == null || !(o instanceof Location))
			{
				return false;
			}
			return ((Location)o).name.equals(this.name);
		}



	} // end of Location class

	public static class OtherWorld implements NamedEntity
	{
		private String name, description;
		private OtherWorldColorSet colorSet;
		private LocationContainer[] segments;
		private int x, y;

		public OtherWorld(String name)
		{
			this.name = name;
			segments = new LocationContainer[2];
			segments[0] = new LocationContainer(getSegmentName(1));
			segments[1] = new LocationContainer(getSegmentName(2));
		}

		public String getName()
		{
			return name;
		}

		/**
		 * Gets segment name of the particular segment of the other world.
		 * Convention is &lt;name&gt;:1 or :2.
		 * @param segment The segment number, either 1 or 2
		 * @return the segment name
		 */
		public String getSegmentName(int segment)
		{
			if (segment < 1 || segment > 2)
			{
				return null;
			}
			return name + ":" + segment;
		}

		/**
		 * Gets the container corresponding to the particular segment
		 * @param segment the segment number, either 1 or 2
		 * @return the location container
		 */
		public LocationContainer getSegment(int segment)
		{
			if (segment < 1 || segment > 2)
			{
				return null;
			}
			return segments[segment - 1];
		}

		/**
		 * Gets the displayed name of this Other World
		 * @return the description
		 */
		public String getDescription() {
			return description;
		}

		/**
		 * Gets the color set of this Other World
		 * @return the colorSet
		 */
		public OtherWorldColorSet getColorSet() {
			return colorSet;
		}


		/**
		 * Gets the x coordinate
		 * @return the x coordinate
		 */
		public int getX() {
			return x;
		}

		/**
		 * Gets the y coordinate
		 * @return the y coordinate
		 */
		public int getY() {
			return y;
		}

		/**
		 * Checks if this other world has a particular gate color
		 * @param color the gate color to check
		 * @return true if it contains the gate color, false otherwise
		 */
		public boolean hasGateColor(GateColor color)
		{
			return colorSet.hasColor(color);
		}

		public String toString()
		{
			return name;
		}

		/**
		 * Creates the data representation of the other world from the XML info and
		 * performs a stringent validation check to make sure the XML data is
		 * valid.
		 * <br>Will return <b>null</b> on a failure.
		 *
		 * @param xmlboard The XML other world info
		 * @return the data representation of the other world
		 */
		public static OtherWorld createOWFromXml(OtherWorldType xmlow)
		{
			OtherWorld ow = new OtherWorld(xmlow.getName());
			if (xmlow.getDescription() != null)
			{
				ow.description = xmlow.getDescription();
			}
			else
			{
				ow.description = ow.name;
			}
			if (!OtherWorldColorSet.isValidOWColorSet(xmlow.getColorset()))
			{
				AHLogger.error("Other World " + ow.name + " has invalid color set");
				return null;
			}
			ow.colorSet = OtherWorldColorSet.getOWColorSet(xmlow.getColorset());
			ow.x = xmlow.getX();
			ow.y = xmlow.getY();

			return ow;
		}
	}

	public static class Path
	{
	    private Location origin;
	    private List<Location> path;
	    private int blockedNodes;

	    public Path(Location origin)
	    {
	        path = new ArrayList<Location>();
	        blockedNodes = 0;
	        this.origin = origin;
	    }

	    /**
         * @return the origin
         */
        public Location getOrigin()
        {
            return origin;
        }

        /**
         * @param origin the origin to set
         */
        public void setOrigin(Location origin)
        {
            this.origin = origin;
        }

        /**
         * @return the path
         */
        public List<Location> getPath()
        {
            return path;
        }
        /**
         * @return the blockedNodes
         */
        public int getBlockedNodes()
        {
            return blockedNodes;
        }
        /**
         * @param blockedNodes the blockedNodes to set
         */
        public void setBlockedNodes(int blockedNodes)
        {
            this.blockedNodes = blockedNodes;
        }

        /**
         * Check if a node in the path is a blocked node
         * @param index the index to check
         * @return true if the node is blocked
         */
        public boolean isNodeBlocked(int index)
        {
            return (blockedNodes > 0 && index + blockedNodes >= path.size());
        }

        /**
         * Gets the last location that is not blocked on this path
         * @return the last unblocked location
         */
        public Location lastUnblockedLocation()
        {
            if (path.size() == 0 || blockedNodes  == path.size())
            {
                return origin;
            }
            return path.get(path.size() - blockedNodes - 1);
        }

        /**
         * Gets the length of the path that is unblocked
         * @return the unblocked length
         */
        public int getUnblockedLength()
        {
            return path.size() - blockedNodes;
        }

        @Override
        public String toString()
        {
            if (path.size() == 0)
            {
                return "Empty path";
            }
            return "Path from " + origin.getName() + " to " + path.get(path.size() - 1).getName() +
             ": -[" + path.toString() + "]- (" + blockedNodes + ")";
        }
	}

	/**
	 * Creates the data representation of the board from the XML info and
	 * performs a stringent validation check to make sure the XML data is
	 * valid.
	 * <br>Will return <b>null</b> on a failure.
	 *
	 * @param xmlboard The XML board info
	 * @return the data representation of the board
	 */
	public static Board createBoardFromXml(BoardType xmlboard)
	{
		Board board = new Board();
		board.name = xmlboard.getName();
		HashSet<String> streetSet = new HashSet<String>();
		/* Add all locations into a list first with all data except
		 * neighborhood and links. */
		for (LocationType loctype : xmlboard.getLocation())
		{
			String locname = loctype.getName();
			if (Board.isRestrictedLocationName(locname))
			{
				AHLogger.error("Locations must not use restricted names.");
				return null;
			}
			if (board.nodes.containsKey(locname))
			{
				AHLogger.error("Duplicate location names in " + board.name + " board.");
				return null;
			}
			Location loc = new Location(locname);
			loc.boardName = board.name;
			loc.stable = loctype.isStable();
			loc.station = loctype.isStation();
			loc.street = loctype.isStreet();
			loc.aquatic = loctype.isAquatic();
			if (loctype.getDescription() != null)
			{
				loc.description = loctype.getDescription();
			}
			else
			{
				loc.description = loc.name;
			}
			if (loctype.getAlttext() != null)
			{
				loc.altText = loctype.getAlttext();
			}
			if (loctype.getAltpower() != null)
			{
				loc.altPower = loctype.getAltpower();
			}
			if (loctype.getAltEvent() != null)
			{
			    loc.altEvent = loctype.getAltEvent();
			}
			loc.x = loctype.getX();
			loc.y = loctype.getY();
			loc.setImage(loctype.getImg());
			if (loc.street)
			{
				streetSet.add(locname);
				loc.color = XmlTools.translateColorString(loctype.getColor());
			}
			loc.getCommonFinds().addAll(loctype.getCommonFind());
			loc.getGuaranteedFinds().addAll(loctype.getGuaranteedFind());
			board.nodes.put(locname, loc);
			board.locations.add(loc);
		}

		board.nodes.put(Board.DUNWICH_VORTEX_NAME, Location.DUNWICH_VORTEX);
		board.nodes.put(Board.INNSMOUTH_VORTEX_NAME, Location.INNSMOUTH_VORTEX);

		/* check that all locations belong to a neighborhood */
		for (LocationType loctype : xmlboard.getLocation())
		{
			String nhood = loctype.getNeighborhood();
			if (!streetSet.contains(nhood))
			{
				AHLogger.error("Location " + loctype.getName() +
					" belongs to a non-existent neighborhood: " + nhood);
				return null;
			}

			if (loctype.isStreet() && !loctype.getName().equals(nhood))
			{
				AHLogger.error("Street " + loctype.getName() + " must have the same " +
					"as its name.");
				return null;
			}
			Location loc = board.nodes.get(loctype.getName());
			loc.neighborhood = nhood;
			/* assign each location to the color of its street(neighborhood) */
			loc.color = board.nodes.get(nhood).color;
		}

		/* Assign links */
		for (LocationType loctype : xmlboard.getLocation())
		{
			Location loc = board.nodes.get(loctype.getName());
			for (LinkType link : loctype.getLink())
			{
				if (!board.nodes.containsKey(link.getTo()))
				{
					AHLogger.error("Location " + loc.getName() +
						" has link to non-existent location " + link.getTo());
					return null;
				}

				Location dest = board.nodes.get(link.getTo());

				loc.getEdges().add(dest);
				loc.getEdgeConnectors().add(new Point(link.getX(), link.getY()));
				if (link.getArrow().equalsIgnoreCase("black"))
				{
					if (loc.blackEdge != null)
					{
						AHLogger.error("Location " + loc.getName() +
							" has more than 1 black arrow.");
						return null;
					}
					loc.blackEdge = dest;
				}
				else if (link.getArrow().equalsIgnoreCase("white"))
				{
					if (loc.whiteEdge != null)
					{
						AHLogger.error("Location " + loc.getName() +
							" has more than 1 white arrow.");
						return null;
					}
					loc.whiteEdge = dest;
				}
				else if (link.getArrow().equalsIgnoreCase("both"))
				{
					if (loc.blackEdge != null)
					{
						AHLogger.error("Location " + loc.getName() +
							" has more than 1 black arrow.");
						return null;
					}
					if (loc.whiteEdge != null)
					{
						AHLogger.error("Location " + loc.getName() +
							" has more than 1 white arrow.");
						return null;
					}
					loc.blackEdge = dest;
					loc.whiteEdge = dest;
				}
				else if (!link.getArrow().equalsIgnoreCase("none"))
				{
					AHLogger.error("Location " + loc.getName() +
						" has an invalid arrow field");
					return null;
				}
			} /* end of each location's links */

			if (loc.blackEdge == null)
			{
				AHLogger.error("Location " + loc.getName() +
					" has no black arrows.");
				return null;
			}
			if (loc.whiteEdge == null)
			{
				AHLogger.error("Location " + loc.getName() +
					" has no white arrows.");
				return null;
			}
		} /* end of all locations */

		board.init(null);
		/* check that all locations are 2 way */
		for (Location loc : board.locations)
		{
			for (Location loc2 : loc.getEdges())
			{
				// check there is an edge back to loc1
				boolean found = false;
				for (Location edge : loc2.getEdges())
				{
					if (edge.getName().equals(loc.getName()))
					{
						found = true;
						break;
					}
				}
				if (!found)
				{
					AHLogger.error("Locations " + loc.getName() +
						" and " + loc2.getName() + " do not have corresponding edges.");
					return null;
				}
			}
		}

		// add other worlds
		for (OtherWorldType xmlow : xmlboard.getOtherWorld())
		{
			OtherWorld ow = OtherWorld.createOWFromXml(xmlow);
			if (ow == null)
			{
				return null;
			}
			board.otherWorlds.add(ow);
		}
		return board;
	}

	/**
	 * Returns true if the name given is a restricted keyword location
	 * @param name The name to check
	 * @return true if this name is restricted, false if it is available for use
	 */
	public static boolean isRestrictedLocationName(String name)
	{
		for (int i = 0; i < RESTRICTED_LOCATION_NAMES.length; i++)
		{
			if (RESTRICTED_LOCATION_NAMES[i].equals(name))
			{
				return true;
			}
		}
		return false;
	}
}
